THREEVec = function(vec) {
    if (vec == undefined)
	return new THREE.Vector3(0,0,0);
    return new THREE.Vector3(vec.x, vec.y, vec.z);
};

View = function(renderer, scene, camera) {
    this.renderer = renderer;
    this.scene = scene;
    this.camera = camera;
    this.objs = [];
};
View.prototype.add = function(obj) {
    if (obj.getMesh) // For camera. Not very elegant
        this.scene.add(obj.getMesh());
    this.objs.push(obj);
};

View.prototype.remove = function(obj) {
    this.scene.remove(obj.getMesh());
    var i = this.objs.indexOf(obj);
    if (i > -1)
	this.objs.splice(i, 1);
    else
	console.error('Trying to remove object that wasn\'t there?');
}

// Simple Base. Currently ctor has nothing important to do so
// there's no need to BaseObjectView.apply(this); in children.
BaseObjectView = function() {}
BaseObjectView.prototype = {
    getPhyObj: function() {return this.phyObj;},
    getMesh: function() {return this.mesh;},
    update: function() {}
};

SolidView = function(solidBoxes) {
    if (!(this instanceof SolidView))
	return new SolidView(solidBoxes);
    
    var mergedGeom;
    var solidbox;

    for (var j = 0; j < solidBoxes.length; j++) {
        solidBox = solidBoxes[j];
        if (solidBox.isHollow)
            continue;

        var points = solidBox.getPhyObj().points;
        var sides = solidBox.getPhyObj().sides;

        var geom = new THREE.Geometry();
        for (var i = 0; i < points.length; i++)
            geom.vertices.push( THREEVec(points[i].pos) );

        for (var i = 0; i < sides.length; i++) {
            geom.faces.push( new THREE.Face3(sides[i][0],
                             sides[i][1],
                             sides[i][2]) );
            geom.faces.push( new THREE.Face3(sides[i][0],
                             sides[i][2],
                             sides[i][3]) );
            geom.computeFaceNormals();
        }
        if (!mergedGeom)
            mergedGeom = geom;
        else {
            THREE.GeometryUtils.merge(mergedGeom, geom);
        }
    }
    
    var material = new THREE.MeshPhongMaterial( { color: 0x3399AA } );
    this.mesh = new THREE.Mesh(mergedGeom, material);
};

SolidView.prototype = Object.create(BaseObjectView.prototype);


CarView = function(car) {
    if (!(this instanceof CarView))
	return new CarView(car);
    this.car = car;
    
    var cube = new THREE.CubeGeometry(this.car.width, 
				      this.car.height, 
				      this.car.length); 
    var material = new THREE.MeshPhongMaterial( { color: 0x999999 } );
    var mesh = new THREE.Mesh(cube, material);
    this.mesh = new THREE.Object3D();
    this.mesh.add( mesh );
};
CarView.prototype = Object.create(BaseObjectView.prototype);

CarView.prototype.update = function() {
    this.mesh.position.x = this.car.centerVec.x;
    this.mesh.position.y = this.car.centerVec.y;
    this.mesh.position.z = this.car.centerVec.z;

    var m  = new THREE.Matrix4();
    var targetV = this.car.centerVec.add(this.car.forwardVec);
    var up = THREEVec(this.car.upVec);
    var target = THREEVec(targetV);

    this.mesh.rotation.setEulerFromRotationMatrix( m.lookAt( target, THREEVec(this.car.centerVec), up ) );  
};

CarView2 = function(car) {
    if (!(this instanceof CarView2))
        return new CarView2(car);
    this.car = car;
    var points = car.getPhyObj().points;

    this.mesh = new THREE.Object3D();
    this.spheres = [];
    var material = new THREE.MeshPhongMaterial( { color: 0x333333 } );
    for (var i = 0; i < points.length; i++) {
	var sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(i<4?2:0.3), material);
	this.spheres.push( sphereMesh );
	this.mesh.add( sphereMesh );
    }
};
CarView2.prototype = Object.create(BaseObjectView.prototype);

CarView2.prototype.update = function() {
    var sphere;
    var points = this.car.getPhyObj().points;
    for (var j = 0; j < this.spheres.length; j++) {
	sphere = this.spheres[j];
	sphere.position.x = points[j].pos.x;
	sphere.position.y = points[j].pos.y;
	sphere.position.z = points[j].pos.z;
	sphere.visible = points[j].groundContact;
    }
}

function repelVec(vec, phyObj) {
        var repelled = false;
        phyObj.testCollision(vec, function(normal, dist) {
            vec.addMe(normal.scale(dist));
            repelled = true;
        });
        return repelled;    
};

function repelVectors(vectors, phyObj) {
    var vs = vectors;
    for (var i = 0; i < vs.length; i++) {
        var vec = Vec(vs[i].x, vs[i].y, vs[i].z);
        vs[i].modified |= repelVec(vec, phyObj);
        vs[i].x = vec.x;
        vs[i].y = vec.y;
        vs[i].z = vec.z;
    }
};

function adjustGroundViewToHeightMap(groundGeom, groundMap, slice) {
    var ctr = 0;
    var vs = groundGeom.vertices;
    var s = slice;

    for (var iz = s.izBegin; iz < s.izEnd; iz += s.izStep) {
        for (var ix = s.ixBegin; ix < s.ixEnd; ix += s.ixStep) {
            vs[ctr].x += s.centerX;
            vs[ctr].z = -vs[ctr].y + s.centerZ;
            vs[ctr++].y = groundMap.get(iz, ix);
        }
    } 
};

function makeHoles(groundGeom, phyObj) {
    var fs = groundGeom.faces;
    var vs = groundGeom.vertices;
    ctr = 0;
    for (var j = 0; j < fs.length - ctr; j++) {
        var f = fs[j];

        var modified = [];
        var unmodified = [];
        var all = [vs[f.a], vs[f.b], vs[f.c], vs[f.d]];
        for (var i = 0; i < all.length; i++) {
            all[i].modified ? modified.push(all[i]) : unmodified.push(all[i]);
        };

        if (modified.length >= 1)
        {
            for (var i = 0; i < unmodified.length; i++) {
                var um = unmodified[i];
                var umVec = Vec(um.x, um.y, um.z);
                var closest = phyObj.closestPos(umVec);
                unmodified[i].x = closest.x;
                unmodified[i].y = closest.y;
                unmodified[i].z = closest.z;
            };
            
            ctr++;
            fs[j] = fs[fs.length-ctr];
            j--; // Replaced face must be considered in next loop           
            
        }
    }

    if (ctr > 0)
        groundGeom.faces = fs.slice(0, -ctr);
}

function createGroundView(groundMap, z, x, LOD, phyObj) {

    // The view will cover point (z,x)
    // Level of detail (LOD) should be an integer > 0. 1 is the most detailed.
    // Holes will be made for hollow objects in phyObj

    var s = groundMap.getSlice(z,x,LOD);
    var groundGeom = new THREE.PlaneGeometry(s.sideLength, s.sideLength, 
                                             s.segments, s.segments);

    adjustGroundViewToHeightMap(groundGeom, groundMap, s);
    repelVectors(groundGeom.vertices, phyObj, s);
    
    groundGeom.computeFaceNormals();
    groundGeom.computeVertexNormals();
    groundGeom.computeTangents();

    makeHoles(groundGeom, phyObj);

    var groundMat = new THREE.MeshLambertMaterial({color: 0x337777});
    var ground = new THREE.Mesh(groundGeom, groundMat);
    //ground.receiveShadow = true;
    return ground;
};

function initView() {
    var ratio = window.innerWidth / window.innerHeight;
    var camera = new THREE.PerspectiveCamera( 75, ratio, 1, 10000 );
    camera.position.set(-2*50, 2*27, 2*13);
    camera.lookAt(new THREE.Vector3(0,0,0));
    var scene = new THREE.Scene();

    // lights
    var ambient = new THREE.AmbientLight( 0x550000 );

    var directionalLight = new THREE.DirectionalLight();
    directionalLight.position.set( 1,1,-1 ).normalize();

    var pointLight = new THREE.PointLight();
    pointLight.position.set( -10,10,0);
    pointLight.intensity = 1;

    scene.add( ambient );
    scene.add( directionalLight );
    scene.add( pointLight );

    var renderer = new THREE.WebGLRenderer();
    renderer.setSize( window.innerWidth, window.innerHeight );
    var debug = $("<div/>").css({position:"absolute"});
    debug.attr("id", "DEBUG");
    $(document.body).append( debug ); 
    document.body.appendChild( renderer.domElement );
    return new View(renderer, scene, camera);
}

var Cam = function(car, ground, entities, camera) {
    this.car = car;
    this.ground = ground;
    this.entities = entities;
    this.camera = camera;
    this.pos = Vec(0,100,-200);
}
Cam.prototype.getFollowPos = function() {

    var collideEntities = function(vec, entities, callback) {
        
        var inHollow = false;
        var newPos;

        for (var i = 0; i < entities.length; i++) {
            
            phyObj = entities[i].getPhyObj();
            
            if (!phyObj.isSolid()) 
                continue;

            phyObj.testCollision(vec, function(normal, penetration, isHollow) {

                if (isHollow)
                    inHollow = true;
                else
                    newPos = vec.add( normal.scale(3 + penetration) ); // 3 for spacing
            });
        }

        callback(newPos, inHollow);
    };

    var collideWorld = function(vec, ground, entities, callback) {
        
        var inHollow = false;
        var newPos;

        collideEntities(vec, entities, function(newPos_, inHollow_) {
            
            newPos = newPos_;
            inHollow = inHollow_;

        });

        if (!inHollow)
            collideVecToGround(newPos || vec, ground, function(h, normal) {

                newPos = Vec(vec.x, h + 3, vec.z); // 3 for spacing

            });

        if (newPos)
            callback(newPos);
    }


    var focus = this.car.centerVec;
    var camdir = this.car.forwardVec.scale( -1 ).add( Vec(0,0.7, 0) ).normalizeMe();

    var steps = 15;
    var camDistance = steps * 3;
    var curr;

    for (var i = 0; i < steps; i++) {
        curr = focus.add( camdir.scale( i * camDistance/steps ) );
        collideWorld(curr, this.ground, this.entities, function(newPos) {
            curr = newPos;
            i = steps; // break
        })
    };

    return curr;

};
Cam.prototype.update = function() {

    var p = this.getFollowPos();
    var weightOldPos = 10;
    this.pos = p.add(this.pos.scale( weightOldPos-1 )).scale( 1/weightOldPos );

    this.camera.position.x = this.pos.x;
    this.camera.position.y = this.pos.y;
    this.camera.position.z = this.pos.z;    
    this.camera.lookAt( this.car.centerVec );
};





var updateView = function(view) {
    for (var i = 0; i < view.objs.length; i++) {
	   view.objs[i].update();
    }
    
    view.renderer.render( view.scene, view.camera );
};

SlaveView = function(car) {
    
    this.car = car;
    
    var width = 5;
    var height = 3;
    var length = 10;
    
    var cube = new THREE.CubeGeometry(width, 
				      height, 
				      length); 
    var material = new THREE.MeshPhongMaterial( { color: 0x3333bb } );
    var mesh = new THREE.Mesh(cube, material);
    this.mesh = new THREE.Object3D();
    this.mesh.add( mesh );
};

SlaveView.prototype.getMesh = function() {
    return this.mesh;
};

SlaveView.prototype.update = function() {
    this.mesh.position.x = this.car.centerVec.x;
    this.mesh.position.y = this.car.centerVec.y;
    this.mesh.position.z = this.car.centerVec.z;
    
    var m  = new THREE.Matrix4();
    var targetV = this.car.centerVec.add(this.car.forwardVec);
    var up = THREEVec(this.car.upVec);
    var target = THREEVec(targetV);

    this.mesh.rotation.setEulerFromRotationMatrix( m.lookAt( target, THREEVec(this.car.centerVec), up ) );  
    
};



SimpleBulletView = function(bullet) {
    this.bullet = bullet;
    
    this.mesh = new THREE.Mesh(
	new THREE.SphereGeometry(1),
	new THREE.MeshPhongMaterial( { color: 0x66bb66 } )
    );
}
SimpleBulletView.prototype = Object.create(BaseObjectView.prototype);

SimpleBulletView.prototype.update = function() {
    var p = this.bullet.getPhyObj().points[0].pos;
    this.mesh.position.x = p.x;
    this.mesh.position.y = p.y;
    this.mesh.position.z = p.z;
};

